package pro.reznick.geoalarm;

import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.graphics.Color;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.media.AudioManager;
import android.os.Bundle;
import android.os.IBinder;
import android.speech.tts.TextToSpeech;

import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;


public class GeoAlarmService extends Service implements ControllerBinderTrait.OnControllerBoundHandler,
        GeoAlarmController.ControllerEventHandler, LocationListener, TextToSpeech.OnInitListener
{


    private ControllerBinderTrait controllerTrait;

    private static final String PROXIMITY_ALERT_ACTION_NAME = "il.ac.mta.geoalarm.ProximityAlert";
    private int minimalUpdateTimeInterval = 30 * 1000; // 30 seconds
    private float minimalUpdateDistanceInterval = 1000.0f; // 1 km
    private LocationManager locationManager;

    private Location lastKnownLocation;
    //private NotificationManager notificationManager;

    private ProximityIntentReceiver proximityReciever = new ProximityIntentReceiver();
    private static final int notificationId = 1001;
    private static final int proximityNotificationId = 1000;

    NotificationManager notificationManager;
    private TextToSpeech tts;
    private HashMap<String, String> ttsParams;
    private boolean ttsInited = false;


    private final Map<Integer, AlarmData> activeAlarms = new HashMap<Integer, AlarmData>();

    @Override
    public IBinder onBind(Intent intent)
    {
        return null;
    }


    @Override
    public int onStartCommand(Intent intent, int flags, int startId)
    {

        controllerTrait = new ControllerBinderTrait(this);
        controllerTrait.controllerEventsHandler = this;
        controllerTrait.bindController();


        Notification ongoingNotification = createOngoingNotification();
        startForeground(notificationId, ongoingNotification);

        if (locationManager == null)
            locationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE);

        notificationManager =
                (NotificationManager) getApplicationContext().getSystemService(Context.NOTIFICATION_SERVICE);

        lastKnownLocation = locationManager.getLastKnownLocation(LocationManager.NETWORK_PROVIDER);
        checkForProximity();
        adjustLocationUpdates();

        //addProximityAlert(32.298415, 34.854779);

        tts = new TextToSpeech(this, this);

        return super.onStartCommand(intent, flags, startId);

    }

    private void adjustLocationUpdates()
    {
        locationManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, minimalUpdateTimeInterval, minimalUpdateDistanceInterval, this);
        locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, minimalUpdateTimeInterval, minimalUpdateDistanceInterval, this);
    }

    private void checkForProximity()
    {
        synchronized (activeAlarms)
        {
            for (AlarmData alarmData : activeAlarms.values())
            {
                if (haversine(lastKnownLocation.getLatitude(), lastKnownLocation.getLongitude(), alarmData.getLatitude(), alarmData.getLongitude())
                        <= ((double) alarmData.getDistanceInMeters() / 1000.0))
                {
                    createProximityAlert(alarmData.getText());
                    controllerTrait.controller.setAlarmState(alarmData, false);
                }
            }
        }
    }

    // from http://www.movable-type.co.uk/scripts/latlong.html
    public static double haversine(double lat1, double lng1, double lat2, double lng2)
    {
        double earthRadius = 6371; // kilometers
        double dLat = Math.toRadians(lat2 - lat1);
        double dLng = Math.toRadians(lng2 - lng1);
        double a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
                Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2)) *
                        Math.sin(dLng / 2) * Math.sin(dLng / 2);
        double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));

        return earthRadius * c;
    }

    private void createProximityAlert(String alarmText)
    {

        Notification notification = new Notification();

        notification.icon = R.drawable.icon;
        notification.when = System.currentTimeMillis();

        notification.flags |= Notification.FLAG_AUTO_CANCEL;
        notification.flags |= Notification.FLAG_SHOW_LIGHTS;

        notification.defaults |= Notification.DEFAULT_VIBRATE;
        //notification.defaults |= Notification.DEFAULT_LIGHTS;
        //notification.defaults |= Notification.DEFAULT_SOUND;

        notification.ledARGB = Color.WHITE;
        notification.ledOnMS = 1500;
        notification.ledOffMS = 1500;


        notification.setLatestEventInfo(getApplicationContext(),
                getString(R.string.ongoing_notification_title),
                alarmText, PendingIntent.getActivity(getApplicationContext(), 0, null, 0));

        notificationManager.notify(proximityNotificationId, notification);
//        notificationManager.notify(notificationId, notification);


        tts.speak(alarmText, TextToSpeech.QUEUE_FLUSH, ttsParams);
    }


    @Override
    public void onControllerBound()
    {
        List<AlarmData> alarms = controllerTrait.controller.getAlarms();
        for (AlarmData alarm : alarms)
            addAlarmPoint(alarm);

        controllerTrait.controller.subscribe(this);
    }

    private void addAlarmPoint(AlarmData alarm)
    {
        if (alarm.isOn())
        {
            synchronized (activeAlarms)
            {
                activeAlarms.put(alarm.getId(), alarm);
            }
        }
    }

    @Override
    public void onAlarmAdded(AlarmData alarm)
    {
        addAlarmPoint(alarm);
    }

    @Override
    public void onAlarmDeleted(AlarmData alarm)
    {

        if (activeAlarms.containsKey(alarm.getId()))
        {
            synchronized (activeAlarms)
            {
                activeAlarms.remove(alarm);
            }
        }
    }

    @Override
    public void onAlarmUpdated(AlarmData alarm)
    {
        synchronized (activeAlarms)
        {
            if (alarm.isOn())
                activeAlarms.put(alarm.getId(), alarm);
            else
                activeAlarms.remove(alarm.getId());
        }
    }


    @Override
    public void onAlarmStateChanged(AlarmData alarm, boolean checked)
    {
        synchronized (activeAlarms)
        {
            if (checked)
                activeAlarms.put(alarm.getId(), alarm);
            else
                activeAlarms.remove(alarm.getId());
        }
    }


    @Override
    public void onDestroy()
    {
        stopForeground(true);
        controllerTrait.unbindController();
        tts.shutdown();
        locationManager.removeUpdates(this);
        super.onDestroy();
    }

    private Notification createOngoingNotification()
    {
        Notification ongoingNotification = new Notification();

        ongoingNotification.icon = R.drawable.toolbar;
        ongoingNotification.when = System.currentTimeMillis();

        ongoingNotification.flags |= Notification.FLAG_AUTO_CANCEL;
        //ongoingNotification.flags |= Notification.FLAG_SHOW_LIGHTS;

        ongoingNotification.flags |= Notification.FLAG_FOREGROUND_SERVICE;
        ongoingNotification.flags |= Notification.FLAG_NO_CLEAR;
        Intent openGeoAlarm = new Intent();
        openGeoAlarm.setClassName("pro.reznick.geoalarm",
                "pro.reznick.geoalarm.GeoAlarmActivity");

        PendingIntent pendingIntent = PendingIntent.getActivity(this, 0,
                openGeoAlarm, 0);

        ongoingNotification.setLatestEventInfo(this,
                getString(R.string.ongoing_notification_title),
                getString(R.string.ongoing_notification_text), pendingIntent);
        return ongoingNotification;
    }

    @Override
    public void onLocationChanged(Location location)
    {
        if (isBetterLocation(location, lastKnownLocation))
        {
            lastKnownLocation = location;
//            Toast.makeText(getApplicationContext(), String.format("new Location (%5f, %5f)", location.getLatitude(), location.getLongitude()), Toast.LENGTH_LONG).show();
        }


        checkForProximity();
    }


    @Override
    public void onStatusChanged(String s, int i, Bundle bundle)
    {
    }

    @Override
    public void onProviderEnabled(String s)
    {
    }

    @Override
    public void onProviderDisabled(String s)
    {
    }

    private static final int TWO_MINUTES = 1000 * 60 * 2;

    /**
     * Determines whether one Location reading is better than the current Location fix
     *
     * @param location            The new Location that you want to evaluate
     * @param currentBestLocation The current Location fix, to which you want to compare the new one
     * @return
     */
    protected boolean isBetterLocation(Location location, Location currentBestLocation)
    {
        if (currentBestLocation == null)
        {
            // A new location is always better than no location
            return true;
        }

        // Check whether the new location fix is newer or older
        long timeDelta = location.getTime() - currentBestLocation.getTime();
        boolean isSignificantlyNewer = timeDelta > TWO_MINUTES;
        boolean isSignificantlyOlder = timeDelta < -TWO_MINUTES;
        boolean isNewer = timeDelta > 0;

        // If it's been more than two minutes since the current location, use the new location
        // because the user has likely moved
        if (isSignificantlyNewer)
        {
            return true;
            // If the new location is more than two minutes older, it must be worse
        }
        else if (isSignificantlyOlder)
        {
            return false;
        }

        // Check whether the new location fix is more or less accurate
        int accuracyDelta = (int) (location.getAccuracy() - currentBestLocation.getAccuracy());
        boolean isLessAccurate = accuracyDelta > 0;
        boolean isMoreAccurate = accuracyDelta < 0;
        boolean isSignificantlyLessAccurate = accuracyDelta > 200;

        // Check if the old and new location are from the same provider
        boolean isFromSameProvider = isSameProvider(location.getProvider(),
                currentBestLocation.getProvider());

        // Determine location quality using a combination of timeliness and accuracy
        if (isMoreAccurate)
        {
            return true;
        }
        else if (isNewer && !isLessAccurate)
        {
            return true;
        }
        else if (isNewer && !isSignificantlyLessAccurate && isFromSameProvider)
        {
            return true;
        }
        return false;
    }

    /**
     * Checks whether two providers are the same
     *
     * @param provider1
     * @param provider2
     * @return
     */
    private boolean isSameProvider(String provider1, String provider2)
    {
        if (provider1 == null)
        {
            return provider2 == null;
        }
        return provider1.equals(provider2);
    }

    @Override
    public void onInit(int i)
    {
        tts.setLanguage(Locale.US);
        ttsParams = new HashMap<String, String>();
        ttsParams.put(TextToSpeech.Engine.KEY_PARAM_STREAM,
                String.valueOf(AudioManager.STREAM_ALARM));

        ttsInited = true;

    }


//    private class Proximity
//    {
//        public double latitude;
//        public double longitude;
//        public double distanceInKm;
//        public String alertText;
//
//        private Proximity(double latitude, double longitude, int distanceInMeters, String alertText)
//        {
//            this.latitude = latitude;
//            this.longitude = longitude;
//            this.distanceInKm = ((double) distanceInMeters / 1000.0);
//            this.alertText = alertText;
//        }
//    }

}
